package protobuf

import (
	"encoding/binary"
	"errors"
	"fmt"
	"go_learn/mlog"
	"go_learn/network"
	"reflect"

	"github.com/golang/protobuf/proto"
)

// -------------------------
// | id | protobuf message |
// -------------------------
type Processor struct {
	littleEndian bool
	id2Msg       map[uint32]*network.MsgInfo
	msg2Id       map[reflect.Type]uint32
}

func NewProcessor() *Processor {
	p := new(Processor)
	p.littleEndian = false
	p.msg2Id = make(map[reflect.Type]uint32)
	p.id2Msg = make(map[uint32]*network.MsgInfo)
	return p
}

// It's dangerous to call the method on routing or marshaling (unmarshaling)
func (p *Processor) SetByteOrder(littleEndian bool) {
	p.littleEndian = littleEndian
}

// It's dangerous to call the method on routing or marshaling (unmarshaling)
func (p *Processor) Register(i *network.MsgInfo) {
	if i.MsgType == nil || i.MsgType.Kind() != reflect.Ptr {
		mlog.Fatalf("protobuf message pointer required")
	}
	if _, ok := p.msg2Id[i.MsgType]; ok {
		mlog.Fatalf("message %s is already registered", i.MsgType)
	}
	p.msg2Id[i.MsgType] = i.ID
	p.id2Msg[i.ID] = i
}

// goroutine safe
func (p *Processor) Unmarshal(data []byte) (interface{}, *network.MsgInfo, error) {
	if len(data) < 4 {
		return nil, nil, errors.New("protobuf data too short")
	}

	// id
	var id uint32
	if p.littleEndian {
		id = binary.LittleEndian.Uint32(data)
	} else {
		id = binary.BigEndian.Uint32(data)
	}
	i, ok := p.id2Msg[id]
	if !ok {
		return nil, nil, fmt.Errorf("message id %v not registered", id)
	}
	msg := reflect.New(i.MsgType.Elem()).Interface()
	return msg, i, proto.UnmarshalMerge(data[4:], msg.(proto.Message))
}

// goroutine safe
func (p *Processor) Marshal(msg interface{}) ([]byte, error) {
	MsgType := reflect.TypeOf(msg)

	// id
	_id, ok := p.msg2Id[MsgType]
	if !ok {
		err := fmt.Errorf("message %s not registered", MsgType)
		return nil, err
	}

	id := make([]byte, 4)
	if p.littleEndian {
		binary.LittleEndian.PutUint32(id, _id)
	} else {
		binary.BigEndian.PutUint32(id, _id)
	}

	// data
	data, err := proto.Marshal(msg.(proto.Message))
	data = append(id, data...)
	return data, err
}
